using System;
using System.Threading;
using System.Collections;
using System.Text;
using System.Net;
using System.Net.Sockets;
using XYThreadPoolLib;
using System.Diagnostics;
using Magnolia;

namespace CommLayer
{
 	public delegate void ExceptionHandlerDelegate(Exception oBug);
	public delegate void ConnectionFilterDelegate(String sRemoteAddress, int nRemotePort, Socket sock);
	public delegate void BinaryInputHandlerDelegate(String sRemoteAddress, int nRemotePort, Byte[] pData);
	public delegate void StringInputHandlerDelegate(String sRemoteAddress, int nRemotePort, String sData);
    //public delegate void ThreadErrorHandlerDelegate(ThreadPoolWorkItem oWorkItem, Exception oError);

	class TcpIpServer
    {

        private delegate void AcceptClientsDelegate();
        private delegate void GetInputDelegate();
        private delegate void ProcessInputDelegate(Transmission trn, IPEndPoint ipe);

        private const int m_nListenBacklog = 32;
        private bool m_bPing = true;

        Socket m_socketServer;
        protected const int m_nServerPause = 25;
        private const int m_nArrayCapacity = 512;
        protected int m_nReadTimeout = 30;
        protected int m_nMaxDataSize = 4 * 1024 * 1024;
        protected int m_nMinThreadCount = 7;
        protected int m_nMaxThreadCount = 12;
        protected String m_sAddress = "";
        protected int m_nPort = 0;
        protected XYThreadPool m_threadPool = new XYThreadPool();
        protected Hashtable m_htTransmissions = new Hashtable(m_nArrayCapacity);
        protected ArrayList m_listTransmissions = new ArrayList(m_nArrayCapacity);
        protected ConnectionFilterDelegate m_delegateConnectionFilter = null;
        protected ExceptionHandlerDelegate m_delegateExceptionHandler = null;
        protected BinaryInputHandlerDelegate m_delegateBinaryInputHandler = null;
        protected StringInputHandlerDelegate m_delegateStringInputHandler = null;
        protected Exception m_exception = null;
        InputProcessor m_inputProcessor;

        /// <summary>
        /// Input processor reference
        /// </summary>
        public InputProcessor InputProcessor
        {

            get
            {
                return m_inputProcessor;
            }
            set
            {
                m_inputProcessor = value;
            }
        }

        /// <summary>
        /// Thread pool
        /// </summary>
        public XYThreadPool ThreadPool
        {
            get
            {
                return m_threadPool;
            }
        }

        /// <summary>
        /// set to true if set server to ping client
        /// </summary>
        /// <returns></returns>
        public bool Ping
        {

            get
            {
                return m_bPing;
            }
            set
            {
                m_bPing = value;
            }
        }

        /// <summary>
        /// ThreadErrorHandler - report back to the excpetion handler
        /// </summary>
        /// <param name="oWorkItem"></param>
        /// <param name="oError"></param>
        protected void ThreadErrorHandler(ThreadPoolWorkItem oWorkItem, Exception oError)
        {
            // create a new exception with new info
            string error = "exception in thread " + oWorkItem.m_sName + " of type " + oError.GetType().ToString() + ": " + oError.Message;
            Exception ex1 = new Exception(error);
            m_delegateExceptionHandler(ex1);


        }
        /// <summary>
        /// Constructor for Tcp server
        /// </summary>
        /// <param name="sAddress"></param>
        /// <param name="port"></param>
        public TcpIpServer(string sAddress, int nPort, int nMinThreadCount, int nMaxThreadCount,
                        ConnectionFilterDelegate delegateConnectionFilter,StringInputHandlerDelegate delegateStringInputHandler,ExceptionHandlerDelegate delegateExceptionHandler)
        {
            m_sAddress = sAddress;
            m_nPort = nPort;

         
		    if(nMinThreadCount>0) 
                m_nMinThreadCount = nMinThreadCount+2;

			if(nMinThreadCount>0&&nMaxThreadCount>nMinThreadCount) 
                m_nMaxThreadCount = nMaxThreadCount+2;
			else
                m_nMaxThreadCount = 2*(m_nMinThreadCount-2)+2;

            // Subsribe to Server Notify events (sent from m_tcpServer)
            SetConnectionFilter(delegateConnectionFilter);
            SetStringInputHandler(delegateStringInputHandler);
            SetExceptionHandler(delegateExceptionHandler);
            m_threadPool.SetThreadErrorHandler(new XYThreadPoolLib.ThreadErrorHandlerDelegate(ThreadErrorHandler));
		}

        /// <summary>
        /// Stops the server
        /// </summary>
        ~TcpIpServer()
		{
			StopServer();
		}

        /// <summary>
        /// retrieves last exception. useful if no exception handler
        /// </summary>
        /// <returns></returns>
		public Exception GetLastException()
		{
			Monitor.Enter(this);
			Exception exp = m_exception;
			Monitor.Exit(this);
			return exp;
		}

        /// <summary>
        /// Closes all sockets
        /// </summary>
		public void StopServer()
		{
			try
    		{
				Monitor.Enter(this);
				m_threadPool.StopThreadPool();
				if(m_socketServer!=null)
				{
					Socket sock = null;
                    for (int i = 0; i < m_listTransmissions.Count; i++)
					{
                        sock = ((Transmission)(m_listTransmissions[i])).m_socket;
						try
						{
							sock.Shutdown(SocketShutdown.Both);
							sock.Close();
						}
						catch(Exception) {}
					}
					try
					{
						m_socketServer.Shutdown(SocketShutdown.Both);
						m_socketServer.Close();
					}
					catch(Exception) {}
				}
			}
#if (!DEBUG)
			catch(Exception) {}
			
#endif		
          finally 
			{
	            {
					m_socketServer = null;
					m_htTransmissions.Clear();
                    m_listTransmissions.Clear();
				}
				
				Monitor.Exit(this); 
			}
				
		}


        /// <summary>
        /// Starts the server
        /// </summary>
        /// <returns></returns>
        public bool StartServer()
        {

            try

            {
                Monitor.Enter(this);
                XYNetCommon.SetSocketPermission();
                StopServer();
                               
                m_threadPool.StartThreadPool(m_nMinThreadCount, m_nMaxThreadCount);

                m_socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                m_socketServer.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
                m_socketServer.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
                m_socketServer.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.BsdUrgent, true);
                m_socketServer.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.Expedited, true);

                // TODO: check without blocking
                m_socketServer.Blocking = true; // TRY making it blocking at the moment
                IPEndPoint myEnd = (m_sAddress == "") ? (new IPEndPoint(Dns.GetHostByName(Dns.GetHostName()).AddressList[0], m_nPort)) : (new IPEndPoint(IPAddress.Parse(m_sAddress), m_nPort));
                m_socketServer.Bind(myEnd);
                m_socketServer.Listen(m_nListenBacklog);

                m_threadPool.InsertWorkItem("Accept Clients", new AcceptClientsDelegate(AcceptClients), null, false);
                m_threadPool.InsertWorkItem("Get Input", new GetInputDelegate(GetInput), null, false);
                return true;
            }
            catch (Exception oBug)
            {
                // trace from here so we can send the line number one day
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> StartServer exception: " + oBug.Message);
                // handle it standradly
                HandleException(oBug,null);
                
                return true;
            }
            
            finally { Monitor.Exit(this); }
            
            
        }

        /// <summary>
        /// Get thread count
        /// </summary>
        /// <returns></returns>
        public int GetThreadCount()
        {
            int nCount = m_threadPool.GetThreadCount() - 2;
            return nCount > 0 ? nCount : 0;
        }

        /// <summary>
        ///  Get client count
        /// </summary>
        /// <returns></returns>
        public int GetClientCount()
        {
            Monitor.Enter(this);
            int nCount = m_htTransmissions.Count;
            Monitor.Exit(this);
            return nCount;
        }
        /// <summary>
        /// Set Max Data Size
        /// </summary>
        /// <param name="nMaxDataSize"></param>
        public void SetMaxDataSize(int nMaxDataSize)
        {
            Monitor.Enter(this);
            if (nMaxDataSize >= 1024) 
                m_nMaxDataSize = nMaxDataSize;
            Monitor.Exit(this);
        }

        /// <summary>
        /// Set connection notify method. To reject a connection request, you simply call the Shutdown 
        /// method and then the Close method on the sock parameter, otherwise connection to the client will be established.
        /// </summary>
        /// <param name="pMethod"></param>
        public void SetConnectionFilter(ConnectionFilterDelegate pMethod)
        {
            Monitor.Enter(this);
            if (m_delegateConnectionFilter == null) 
                m_delegateConnectionFilter = pMethod;
            Monitor.Exit(this);
        }

        /// <summary>
        /// This is the user defined error handler, it is set by the 
        /// SetExceptionHandler method. The oBug parameter is an exception object representing the server error
        /// </summary>
        /// <param name="pMethod"></param>
        public void SetExceptionHandler(ExceptionHandlerDelegate pMethod)
        {
            Monitor.Enter(this);
            if (m_delegateExceptionHandler == null) m_delegateExceptionHandler = pMethod;
            Monitor.Exit(this);
        }

        /// <summary>
        /// When a binary message from a client is received, this handler will be invoked automatically
        /// </summary>
        /// <param name="pMethod"></param>
        public void SetBinaryInputHandler(BinaryInputHandlerDelegate pMethod)
        {
            Monitor.Enter(this);
            if (m_delegateBinaryInputHandler == null) m_delegateBinaryInputHandler = pMethod;
            Monitor.Exit(this);
        }

        /// <summary>
        /// Setter for callback function
        /// </summary>
        /// <param name="pMethod"></param>
        public void SetStringInputHandler(StringInputHandlerDelegate pMethod)
        {
            Monitor.Enter(this);
            if (m_delegateStringInputHandler == null) m_delegateStringInputHandler = pMethod;
            Monitor.Exit(this);
        }


        #region Thread Procedures
        /// <summary>
        /// Thread proc for accepting client connections
        /// </summary>
        private void AcceptClients()
        {
            while (true)
            {
                bool bHasNewClient = false;
                Socket sock = null;
                Transmission trn = null;
                IPEndPoint ipe = null;
                try
                {
                    // try to see whether we got a connection. 
                    // TODO: try accepting with a -1 polling
                    if (m_socketServer.Poll(m_nServerPause, SelectMode.SelectRead))
                    {
                        bHasNewClient = true;
                        // getting the socket
                        sock = m_socketServer.Accept();
                        ipe = (IPEndPoint)(sock.RemoteEndPoint);
                        if (m_delegateConnectionFilter != null)
                        {
                            // notifying connection through the connection filter call backk
                            m_threadPool.InsertWorkItem("Connection established", new ConnectionFilterDelegate(m_delegateConnectionFilter), new Object[3]{ ipe.Address.ToString(), ipe.Port, sock },false);
                        }
                        
                        // if indeed it is still connected
                        if (sock.Connected)
                        {
                            String sKey = ipe.Address.ToString() + ":" + ipe.Port.ToString();
                            Monitor.Enter(this);
                            trn = new Transmission(sock);
                            // add a socket to the socket list
                            m_htTransmissions.Add(sKey, trn);
                            m_listTransmissions.Add(trn);

                            // set keep alive on this connection. 
                            XYNetCommon.SetKeepAlive(1, sock);


                            // TODO: force context switch
                            Monitor.Exit(this);
                        }
                        else
                            throw new Exception("connection lost right after accepting");
                    }
                }

                catch (Exception oBug)
                {
                     // trace from here so we can send the line number one day
                    Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> AcceptClients() exception: " + oBug.Message);
                    // and handle
                    HandleException(oBug,trn);
                }

                
                Thread.Sleep(m_nServerPause);
            }
        }
        
        /// <summary>
        /// Thread proc for getting socket data
        /// </summary>
        private void GetInput()
        {
            int nCounter = 0;
            while (true)
            {
                nCounter++;
                bool bNoData = true;
                try
                {
                    for (int i = m_listTransmissions.Count - 1; i >= 0; i--)
                    {
                        Socket sock = null;
                        IPEndPoint ipe = null;
                        Transmission trnsmsn = null;

                        try
                        {
                            sock = ((Transmission)(m_listTransmissions[i])).m_socket;
                            trnsmsn = (Transmission)m_listTransmissions[i];
                            ipe = (IPEndPoint)(sock.RemoteEndPoint);
                            //if (!sock.Connected) 
                            //    throw new Exception("Connection to client closed");
                            // ping socket every 1000?
                            if (Ping && nCounter % 1000 == 0)
                            {
                                if (sock.Send(new Byte[4] { 2, 0, 0, 0 }) != 4)
                                    throw new Exception("Failed to ping client socket");
                            }
                            
                            // if there is available data
                            if (sock.Available > 0)
                            {
                                Monitor.Enter(this);
                                // remove from list (needed so input processing done on separate thread)
                                m_listTransmissions.RemoveAt(i);
                                Monitor.Exit(this);
                                // signal data
                                bNoData = false;
                                
                                // process the input (will re-add the socket, too)
                                //m_threadPool.InsertWorkItem("Process Input", new ProcessInputDelegate(m_inputProcessor.ProcessInput), new Object[2] { trnsmsn, ipe }, false);
                                m_inputProcessor.Start(trnsmsn, ipe);
                            }
                        }

                        catch (Exception oBug)
                        {
                            // trace from here so we can send the line number one day
                            Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> GetInput() exception:" + oBug.Message);
                            // and handle
                            HandleException(oBug,trnsmsn);
                        }
                    }
                    
                    // if there is no data, wait longer?
                    /*if (bNoData)
                        Thread.Sleep(10 * m_nServerPause);
                    else */
                    Thread.Sleep(m_nServerPause);
                }
                catch (Exception) { }
            }
        }
  
        #endregion

        #region send functions

        /// <summary>
        /// Sends byte data
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="pData"></param>
        /// <returns></returns>
        protected bool SendRawData(String sRemoteAddress, int nRemotePort, Byte[] pData)
        {
            if (sRemoteAddress == null || pData == null) 
                return false;
            Socket sock = null;
            Transmission trn = null;
            try
            {
                Monitor.Enter(this);
                // get the socket by its address and port
                trn = (Transmission)(m_htTransmissions[sRemoteAddress + ":" + nRemotePort.ToString()]);
                sock = trn.m_socket;
                if (sock == null)
                    throw new Exception("No client connection at the given address and port");
                
                // check if the client socket is being used for receiving. Probably that should not prevent us from sending on it
                if (m_listTransmissions.Contains(trn) == false)
                {
                    Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceWarning,  DateTime.Now.ToLongTimeString() + "-> " + XYNetCommon.GetTimeString() + sRemoteAddress + ":" + nRemotePort.ToString() + " - Client socket in use");
                    //sock = null;
                    //throw new Exception("Client socket in use");
                }

                XYNetCommon.SendData(pData, sock, 0);

            }

            catch (Exception oBug)
            {
                // trace from here so we can send the line number one day
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> SendRawData to " + sRemoteAddress + " exception " + oBug.Message);
                // and handle exception regularly
                HandleException(oBug, trn);
                return false;
            }
            
           finally {
                Monitor.Exit(this);  }
            
            return true;
            
        }

        /// <summary>
        /// sends binary data with length header
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="pData"></param>
        /// <returns></returns>
        public bool SendBinaryData(String sRemoteAddress, int nRemotePort, Byte[] pData)
        {
            Byte[] pData2 = new Byte[pData.Length + 4];
            pData2[0] = (Byte)(1 + (pData.Length / 16777216/*2^24*/) * 16); // adds one to signal that this is binary. But - if we add 1 to signal that we sent binary, we need to remove it, and it doesn't look this way
            pData2[1] = (Byte)(pData.Length % 256);
            pData2[2] = (Byte)((pData.Length % 65536) / 256);
            pData2[3] = (Byte)((pData.Length / 65536) % 256);
            pData.CopyTo(pData2, 4);
            return SendRawData(sRemoteAddress, nRemotePort, pData2);
        }

        /// <summary>
        /// Send string data with length header. assumes 2-byte characters
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="sData"></param>
        /// <returns></returns>
        public bool SendStringData(String sRemoteAddress, int nRemotePort, String sData)
        {
            Byte[] pData = new Byte[sData.Length * 2 + 4];
            pData[0] = (Byte)(((2 * sData.Length) / 16777216 /*2^24*/) * 16);
            pData[1] = (Byte)((2 * sData.Length) % 256);
            pData[2] = (Byte)(((2 * sData.Length) % 65536) / 256);
            pData[3] = (Byte)(((2 * sData.Length) / 65536) % 256);
            XYNetCommon.StringToBinary(sData).CopyTo(pData, 4);
            return SendRawData(sRemoteAddress, nRemotePort, pData);
        }

        /// <summary>
        /// Send string data to everyone conencted
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="sData"></param>
        /// <returns></returns>
        public virtual bool SendStringData(String sData)
        {
            bool bRet = true;

            try
            {
                if (m_htTransmissions.Count == 0)
                {
                    throw new Exception("no active connections");
                }

                // get the address of the sender (get the first client that was connected)
                foreach (Transmission trn in m_htTransmissions)
                {
                    Socket sock = trn.m_socket;
                    IPEndPoint ipe = (IPEndPoint)(sock.RemoteEndPoint);
                    bool bRetLocal = SendStringData(ipe.Address.ToString(), ipe.Port, sData);
                    bRet = bRetLocal && bRet;
                }
            }
            catch(Exception oBug)
            {
                // trace from here so we can send the line number one day
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> exception: SendStringData(" + sData + "):" + oBug.Message);

                // and handle exception regularly
                HandleException(oBug, null);

                return false;
            }
            return bRet;
        }

        /// <summary>
        /// Handle server exception
        /// </summary>
        public void HandleException(Exception oBug, Transmission trn)
        {
            try
            {
                Monitor.Enter(this);
                // report excpetion
                m_threadPool.InsertWorkItem("Handle Exception", m_delegateExceptionHandler, new Object[1] { oBug }, false);
                if (trn != null)
                {
                    // remove from thread lists 
                    IPEndPoint ipe = (IPEndPoint)(trn.m_socket.RemoteEndPoint);
                    string strKey = ipe.Address.ToString() + ":" + ipe.Port.ToString();
                    if (m_htTransmissions.Contains(strKey))
                    {
                        m_htTransmissions.Remove(strKey);
                    }

                    if (m_listTransmissions.Contains(trn))
                    {
                        m_listTransmissions.Remove(trn);
                    }

                    // and shutdown
                    trn.m_socket.Shutdown(SocketShutdown.Both);
                    trn.m_socket.Close();

                }
            }
            catch (Exception oBug1)
            {
                // trace from here too
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> EXCEPTION IN HandleException!!:" + oBug1.Message);
                // and handle it with a direct call
                m_delegateExceptionHandler(oBug1);
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

#endregion

        
    }
}
